# -*-coding:utf-8-*-

import base64
import hashlib
import json
import random
import re
import time
import cStringIO
import uuid

import qrcode

import tornado
from bson.objectid import ObjectId
from tornado.gen import coroutine, Task

import settings
from alidayu.topnado import Topnado
from handlers.base import BaseHandler
from handlers.base import Wait
from pyrestful import mediatypes
from pyrestful.rest import get, post, put, delete
from utils.auth import authenticated, generate_token
from utils.sign import signed


# TODO: WEB 环境中使用 OAuth 方式
# TODO: 支持用户名，手机号多种登录方式

class UsersHandler(BaseHandler):
    def __init__(self, application, request, **kwargs):
        super(UsersHandler, self).__init__(application, request, **kwargs)
        self.wait_id = None

    def check_permissions(self, permissions):
        return True

    def check_user_id_from_path(self, user_id):
        if user_id != self.user_id:
            self.respond_error('invalid_path', 'user id not match')

    def get_code_key(self, key_prefix, recnum):
        return '%s:%s:%s' % (self.app_id, key_prefix, recnum)

    @coroutine
    def check_verify_code(self, key_prefix, recnum, code):
        key = self.get_code_key(key_prefix, recnum)
        result = yield Task(self.redis_client.get, key)
        if result != str(code):
            self.respond_error('invalid_sms_code', 'invalid code or code expired')
        # 写入无效的验证码，保留 key 用于控制发送频率
        invalid_code = random.randint(100000, 999999)
        yield Task(self.redis_client.set, key, invalid_code)

    def phonecheck(self, s):
        phoneprefix = ['130', '131', '132', '133', '134', '135', '136', '137', '138', '139',
                       '150', '151', '152', '153', '156', '158', '159',
                       '170',
                       '183', '182', '185', '186', '188', '189']
        # return (len(s) == 11 and s.isdigit() and s[:3] in phoneprefix)
        return (len(s) == 11 and s.isdigit())

    def validate_phone(self, recnum):
        if not self.phonecheck(recnum):
            self.respond_error('invalid_receive_number')

    def validate_email(self, email):
        if len(email) < 7 or re.match("^.+\\@(\\[?)[a-zA-Z0-9\\-\\.]+\\.([a-zA-Z]{2,3}|[0-9]{1,3})(\\]?)$",
                                      email) is None:
            self.respond_error('invalid_email_format')

    @coroutine
    def send_verify_code(self, send_func, key_prefix):
        recnum = str(self.get_argument('recnum'))
        self.validate_phone(recnum)
        code = random.randint(100000, 999999)
        key = self.get_code_key(key_prefix, recnum)
        code_timeout = settings.SMS_CODE_TIMEOUT
        if self.prod:
            result = yield Task(self.redis_client.ttl, key)
            if result is None:
                result = 0
            if code_timeout - result < settings.SMS_CODE_INTERVAL:
                self.respond_error('sms_code_send_fail', 'sms request too often')
        else:
            code_timeout = settings.TEST_SMS_CODE_TIMEOUT
        yield Task(self.redis_client.setex, key, code_timeout, code)

        if self.prod:
            result = send_func(self.app, recnum, code)
            if not result[0]:
                yield Task(self.redis_client.delete, key)
                self.respond_error('sms_code_send_fail', result[1])
            else:
                self.respond_success()
        else:
            result = {'code': code}
            self.respond_success(result)

    def generate_token_result(self, user_id):
        result = dict(user_id=user_id)
        (result['access_token'], result['expires_at']) = generate_token(self.app, user_id, self.request.remote_ip)
        return result

    def return_token_result(self, user_id):
        result = self.generate_token_result(user_id)
        self.set_cookie('access_token', result['access_token'], expires=result['expires_at'])
        self.respond_success(result)

    @get(_path='/api/users/register/code')
    @signed(sign_from_body=False)
    @coroutine
    def send_register_code(self):
        phone_no = str(self.get_argument('recnum'))
        result = yield self.users_dao.get_user_by_phone_no(self.app, phone_no)
        if result is not None:
            self.respond_error('user_already_exists')
        yield self.send_verify_code(Topnado().send_register_code, 'register_code')

    @delete(_path='/test/api/users/{username}', _types=[str])
    @signed(sign_from_body=False)
    @coroutine
    def delete_user(self, username):
        if self.prod:
            raise Exception('This is a test API only for unit test phase!')
        result = yield self.users_dao.delete_user_by_username(self.app, username)
        self.respond_success(result)

    @post(_path='/api/users/register', _types=[dict],
          _consumes=mediatypes.APPLICATION_JSON)
    @signed(sign_from_body=True)
    @coroutine
    def register(self, request):
        required_fields = ['username', 'password', 'code']
        optional_fields = ['specifics']
        request = self.extract_fields(request, required_fields=required_fields, optional_fields=optional_fields)
        specifics = request.pop('specifics') if 'specifics' in request else {}

        kwargs = dict(specifics=specifics)
        specifics = yield self.delegate_pre_create('users', **kwargs)
        if specifics is not None:
            request['specifics'] = specifics

        yield self.check_verify_code('register_code', request['username'], request['code'])
        result, user = yield self.users_dao.add_user(self.app, request)
        if not result:
            self.respond_error('user_already_exists')
        self.respond_success(user)

    @get(_path='/api/users/password/code')
    @signed(sign_from_body=False)
    @coroutine
    def send_reset_password_code(self):
        phone_no = str(self.get_argument('recnum'))
        result = yield self.users_dao.get_user_by_phone_no(self.app, phone_no)
        if result is None:
            self.respond_error('user_not_exists')
        yield self.send_verify_code(Topnado().send_reset_password_code, 'reset_password_code')

    @put(_path='/api/users/password', _types=[dict],
         _consumes=mediatypes.APPLICATION_JSON)
    @signed(sign_from_body=True)
    @coroutine
    def reset_user_password(self, request):
        request = self.extract_fields(request, required_fields=['username', 'password', 'code'])
        username = request['username']
        password = request['password']
        code = request['code']
        yield self.check_verify_code('reset_password_code', username, code)
        result = yield self.users_dao.update_one(
            {
                '$or': [
                    {'username': username},
                    {'phone': username}
                ],
                'app_id': self.app.id_str},
            {'$set': {'password': password}})
        if 0 == result.matched_count:
            self.respond_error('user_not_exists')
        self.respond_success(True)

    @get(_path='/api/users/login/guest')
    @signed(sign_from_body=False)
    @coroutine
    def login_guest(self):
        if not self.app.guest_enable:
            raise Exception('Guest is not enable for your APP')
        user = yield self.users_dao.get_user_by_id(self.app, self.app.guest_id)
        if user is None:
            specifics = dict(vendor_id=self.app.developer_id)
            data = dict(tags=['guest'], username='guest', specifics=specifics, _id=ObjectId(self.app.guest_id))
            result, user = yield self.users_dao.add_user(self.app, data)
            if not result:
                self.logger.info('add guest user fail')
            user_id = user['user_id']
        else:
            user_id = user.id_str
        self.return_token_result(user_id)

    @get(_path='/api/users/login')
    @signed(sign_from_body=False)
    @coroutine
    def login(self):
        kwargs = self.query_arguments()
        kwargs = self.extract_fields(kwargs, required_fields=['username', 'password'])
        username = kwargs['username']
        password = kwargs['password']
        user = yield self.users_dao.get_user_by_phone_no(self.app, username)
        if user is None:
            self.respond_error('user_not_exists')
        if str(password) != user.password:
            self.respond_error('wrong_password')
        self.return_token_result(user.id_str)

    @get(_path='/api/users/bind/code')
    @signed(sign_from_body=False)
    @coroutine
    def send_bind_code(self):
        yield self.send_verify_code(Topnado().send_register_code, 'bind_code')

    @post(_path='/api/users/bind/consumers', _types=[dict],
          _consumes=mediatypes.APPLICATION_JSON)
    @signed(sign_from_body=False)
    @coroutine
    def bind_consumers(self, request):
        user_bind_info_cookie = self.get_cookie('user_bind_info')
        if user_bind_info_cookie is None:
            raise Exception('user_bind_info_cookie is None, maybe already bound')
        user_bind_info = json.loads(base64.urlsafe_b64decode(user_bind_info_cookie))

        if 'weixin' in user_bind_info:
            username = user_bind_info['weixin']['mp']['openid']
        elif 'alipay' in user_bind_info:
            raise Exception('alipay consumer binding still not supported')
        else:
            raise Exception('unsupported binding method')
        #request.update(tags=['consumer'], username=username, **user_bind_info)

        specifics = dict(vendor_id=self.app.developer_id)
        data = dict(tags=['guest'], username=username, specifics=specifics, **user_bind_info)
        # request.update(tags=['guest'], username=username, **user_bind_info)
        result, user = yield self.users_dao.add_user(self.app, data)
        if not result:
            self.logger.info('consumer already exists')
        user_id = user['user_id']
        self.return_token_result(user_id)

    @post(_path='/api/users/bind', _types=[dict],
          _consumes=mediatypes.APPLICATION_JSON)
    @signed(sign_from_body=False)
    @coroutine
    def bind(self, request):
        required_fields = ['username', 'code']
        optional_fields = ['specifics']
        request = self.extract_fields(request, required_fields=required_fields, optional_fields=optional_fields)

        username = request['username']

        yield self.check_verify_code('bind_code', username, request['code'])
        user_bind_info_cookie = self.get_cookie('user_bind_info')
        if user_bind_info_cookie is None:
            raise Exception('user_bind_info_cookie is None, maybe already bound')
        user_bind_info = json.loads(base64.urlsafe_b64decode(user_bind_info_cookie))

        user = yield self.users_dao.get_user_by_phone_no(self.app, username)
        if user is None:
            specifics = request.pop('specifics', {})
            kwargs = dict(specifics=specifics)
            specifics = yield self.delegate_pre_create('users', **kwargs)
            if specifics is not None:
                request['specifics'] = specifics
            request.update(**user_bind_info)
            result, user = yield self.users_dao.add_user(self.app, request)
            user_id = user['user_id']
        else:
            if 'weixin' in user_bind_info and user.weixin is not None:
                raise Exception('User(%s) had already bound another Wechat' % username)
            yield self.users_dao.update_user(user.id_str, user_bind_info)
            user_id = user.id_str
        self.return_token_result(user_id)

    @delete(_path='/api/users/{user_id}/weixin', _types=[str])
    @authenticated
    @coroutine
    def unbind(self, user_id):
        self.check_user_id_from_path(user_id)
        result = yield self.users_dao.update_one({'_id': ObjectId(self.user_id)}, {'$unset': {'weixin': 1}})
        result = (0 == result.matched_count)
        if result:
            self.set_cookie('access_token', '')
        self.respond_success(result)

    @put(_path='/api/users/{user_id}/password', _types=[str],
         _consumes=mediatypes.APPLICATION_JSON)
    @authenticated
    @coroutine
    def update_user_password(self, user_id, request):
        self.check_user_id_from_path(user_id)
        password_old = request['password_old']
        password_new = request['password_new']
        result = yield self.users_dao.update_one({'_id': ObjectId(user_id), 'password': password_old},
                                                 {'$set': {'password': password_new}})
        if 0 == result.matched_count:
            self.respond_error('user_not_exists')
        self.respond_success(True)

    @get(_path='/api/users/{user_id}', _types=[str])
    @authenticated
    @coroutine
    # 注意: 这里使用 get_user 函数名会导致奇怪的错误
    # def get_user(self, user_id):
    def retrieve_user(self, user_id):
        self.check_user_id_from_path(user_id)
        projection = {'_id': 0, 'password': 0, 'app_id': 0}
        user = yield self.users_dao.get_user_raw(self.app, user_id=user_id, projection=projection)
        if user is None:
            self.respond_error('user_not_exists')

        # 使用有机会取得最新的关联信息,比如推广者的新修改的手机号或头像等
        specifics = user['specifics'] if 'specifics' in user else None
        delegate = self.get_delegate('users')
        specifics = delegate.on_post_get(specifics=specifics)
        if isinstance(specifics, tornado.concurrent.Future):
            specifics = yield specifics
        if specifics is not None:
            user['specifics'] = specifics

        user['created_time'] = int(time.mktime(ObjectId(user_id).generation_time.timetuple()))
        self.respond_success(user)

    @put(_path='/api/users/{user_id}', _types=[str, dict],
         _consumes=mediatypes.APPLICATION_JSON)
    @authenticated
    @coroutine
    def update_user(self, user_id, request):
        self.check_user_id_from_path(user_id)
        request = self.extract_fields(request, optional_fields=['name', 'email', 'address'])
        if 'email' in request:
            self.validate_email(request['email'])
        result = yield self.users_dao.update_one({'_id': ObjectId(user_id)}, {'$set': request})
        if 0 == result.matched_count:
            self.respond_error('user_not_exists')
        self.respond_success()

    @get(_path='/api/users/{user_id}/phone/code', _types=[str])
    @authenticated
    @coroutine
    def send_change_phone_code(self, user_id):
        self.check_user_id_from_path(user_id)
        yield self.send_verify_code(Topnado().send_change_phone_code, 'change_phone_code')

    @put(_path='/api/users/{user_id}/phone', _types=[str, dict],
         _consumes=mediatypes.APPLICATION_JSON)
    @authenticated
    @coroutine
    def update_user_phone(self, user_id, request):
        self.check_user_id_from_path(user_id)
        request = self.extract_fields(request, required_fields=['phone', 'code'], optional_fields=['spreader'])
        # TODO: confirm the reason why to to the following
        # self.check_and_insert_spread_info(request)
        yield self.check_verify_code('change_phone_code', request['phone'], request['code'])
        del request['code']
        if 'spreader' in request:
            del request['spreader']
        result = yield self.users_dao.update_one({'_id': ObjectId(user_id)}, {'$set': request})
        if 0 == result.matched_count:
            self.respond_error('user_not_exists')
        self.respond_success()

    @get(_path='/api/users/auth/qrcode')
    def get_auth_qrcode(self):
        appkey = self.get_argument('appkey')
        wait_id = hashlib.md5(str(uuid.uuid4())).hexdigest()
        url = '%s://%s/mp/auth?appkey=%s&wait_id=%s' % (
            self.request.protocol, self.request.host, appkey, wait_id)
        self.logger.info(url)
        img = qrcode.make(url, box_size=5)
        output = cStringIO.StringIO()
        img.save(output, 'PNG')
        img_data = output.getvalue()
        output.close()
        self.set_header('Content-Type', 'image/png')
        self.set_cookie('wait_id', wait_id, expires=time.time() + 60)
        self.finish(img_data)

    def on_connection_close(self):
        if self.wait_id is not None:
            self.mqtornado.wait_response_cancel(self.wait_id)

    def _on_complete(self, result):
        self.respond_success(result)

    def _on_timeout(self):
        response = self.make_error_response('mq_request_timeout')
        self.finish(response)

    # NOTE: 两 decorator 位置互换后逻辑仍正常!
    @tornado.web.asynchronous
    @get(_path='/api/users/auth/result')
    def get_auth_result(self):
        self.wait_id = self.get_cookie('wait_id')
        if self.wait_id is None:
            # NOTE: 异步方式的异常捕抓路径与同步方式不同
            # raise Exception('wait id do not present as cookie, please call /mp/auth/qrcode API first')
            self.finish('wait id do not present as cookie, please call /mp/auth/qrcode API first')
        else:
            wait = Wait(self._on_complete, self._on_timeout, expires_in=60)
            self.mqtornado.wait_response(self.wait_id, wait)

    '''
    Addresses of User
    '''

    @get(_path='/api/users/{uid}/addresses', _types=[str])
    @authenticated
    @coroutine
    def get_user_address_list(self, uid):
        self.check_user_id_from_path(uid)
        result = yield self.users_dao.get_user_address_list(uid)
        if result is None:
            result = []
        self.respond_success(result)

    @post(_path='/api/users/{uid}/addresses', _types=[str],
          _consumes=mediatypes.APPLICATION_JSON)
    @authenticated
    @coroutine
    def add_user_address(self, uid, request):
        self.check_user_id_from_path(uid)
        result = yield self.users_dao.add_user_address(uid, request)
        self.respond_success(result)

    @put(_path='/api/users/{uid}/addresses/{aid}', _types=[str, str],
         _consumes=mediatypes.APPLICATION_JSON)
    @authenticated
    @coroutine
    def update_user_address(self, uid, aid, request):
        self.check_user_id_from_path(uid)
        result = yield self.users_dao.update_user_address(uid, aid, request)
        self.respond_success(result)

    @get(_path='/api/users/{uid}/addresses/{aid}', _types=[str, str])
    @authenticated
    @coroutine
    def get_user_address(self, uid, aid):
        self.check_user_id_from_path(uid)
        result = yield self.users_dao.get_user_address(uid, aid)
        self.respond_success(result)

    @delete(_path='/api/users/{uid}/addresses/{aid}', _types=[str, str])
    @authenticated
    @coroutine
    def delete_user_address(self, uid, aid):
        self.check_user_id_from_path(uid)
        result = yield self.users_dao.delete_user_address(uid, aid)
        self.respond_success(result)

        # @get(_path='/api/users/maintain_and_get_status')
        # @coroutine
        # def maintain_and_get_status(self):
        #     u = 0
        #     filter_cond = {'specifics.spreader': {'$exists': 1}}
        #     cursor = self.users_dao.find(filter_cond, {'specifics': 1})
        #     while (yield cursor.fetch_next):
        #         r = cursor.next_object()
        #         spreader_info = r['specifics']['spreader']
        #         if isinstance(spreader_info, dict):
        #             result = yield self.users_dao.update_one(r, {'$set': {'specifics.spreader': spreader_info['phone']}})
        #             u += result.matched_count
        #     self.respond_success(u)
        #
        # @get(_path='/api/users/maintain_and_get_status/product_and_order')
        # @coroutine
        # def maintain_product_order_and_get_status(self):
        #     # filter_cond = {'type': {'$exists': 1}}
        #     filter_cond = {'type': 'service'}
        #     cursor = self.products_dao.find(filter_cond, {'_id': 1})
        #     service_pids = []
        #     service_pids_str = []
        #     while (yield cursor.fetch_next):
        #         r = cursor.next_object()
        #         service_pids.append(r['_id'])
        #         service_pids_str.append(str(r['_id']))
        #     service_prod_count = yield self.products_dao.count({'_id': {'$in': service_pids}})
        #     goods_prod_count = yield self.products_dao.count({'_id': {'$not': {'$in': service_pids}}})
        #
        #     result = yield self.products_dao.update_many({'_id': {'$in': service_pids}}, {'$set': {'type': 'service'}})
        #     serv_prod_update_count = result.matched_count
        #     result = yield self.products_dao.update_many({'_id': {'$not': {'$in': service_pids}}}, {'$set': {'type': 'goods'}})
        #     goods_prod_update_count = result.matched_count
        #
        #     order_total = yield self.orders_dao.count()
        #     service_order_total = yield self.orders_dao.count({'products.product_id': {'$in': service_pids_str}})
        #     goods_order_total = yield self.orders_dao.count({'products.product_id': {'$not': {'$in': service_pids_str}}})
        #
        #     result = yield self.orders_dao.update_many({'products.product_id': {'$in': service_pids_str}}, {'$set': {'type': 'service'}})
        #     serv_order_update_count = result.matched_count
        #     result = yield self.orders_dao.update_many({'products.product_id': {'$not': {'$in': service_pids_str}}}, {'$set': {'type': 'goods'}})
        #     goods_order_update_count = result.matched_count
        #
        #     result = {
        #         'service_pids_str': service_pids_str,
        #         'service_prod_count': service_prod_count,
        #         'goods_prod_count': goods_prod_count,
        #         'order_total': order_total,
        #         'service_order_total': service_order_total,
        #         'goods_order_total': goods_order_total,
        #         'serv_prod_update_count': serv_prod_update_count,
        #         'goods_prod_update_count': goods_prod_update_count,
        #         'serv_order_update_count': serv_order_update_count,
        #         'goods_order_update_count': goods_order_update_count,
        #     }
        #     self.respond_success(result)
